Java的异常处理机制主要依赖于try、catch、finally、throw和throws五个关键字。其中:
1) try关键字后紧跟一个花括号括起来的代码块(即try块),里面放置可能引发异常的代码。
2) catch后对应异常类型和一个代码块,表明该catch块用于处理这种类型的代码块。
3) 多个catch块后可以跟一个finally块,finally块用于回收在try块中打开的物理资源,异常机制会保证finally块总是被执行。
4) throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象。
5) throws主要在方法签名中使用,用于声明该方法可能抛出的异常。
异常处理机制
将“错误处理代码”从“业务实现代码”中分离出来。
使用try…catch捕获异常
将业务处理代码放在try块中定义,所有的异常处理逻辑放在catch块中进行。伪代码如下:
1 | try{ |
出现异常->生成异常对象e->提交给JRE->交给catch块处理(即捕获异常,e对象被传入)->如果没有合适的catch块程序就此退出
不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行改代码块出现了异常,系统总会生成一个异常对象。该异常对象被提交给Java运行时环境(JRE)(即抛出(throw)异常)。JRE寻找合适的catch块,把该异常对象交给catch块处理(即捕获(catch)异常),catch块处理该异常类及其子类的异常是,如果程序没有为这段代码定义任何的catch块,程序就在此退出。
1 | try{ |
Java的所有非正常情况分为错误(Error)和异常(Exception),而Exception又可分为Checked异常和Runtime异常(运行时异常)。Error一般指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失效等,这些错误无法恢复或不可能捕获,将导致应用程序中断。
先处理小异常,再处理大异常。
Java7提供多异常捕获
一个catch块可以捕获多种类型的异常,需注意:
1)多种异常类型之间用|隔开。
2)捕获多种异常时,异常变量有隐式的final修饰,因此不能对异常变量重新赋值。
访问异常信息
当java决定调用某个catch块来处理该异常对象时,会将异常对象赋值给catch块后面的异常参数,程序即可通过该参数来获得异常的相关信息。
使用finally回收资源
程序在try块里打开了一些物理资源(如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显示回收。
Java的垃圾回收机制(是一种后台线程)只能回收堆内存中对象所占的资源,不会回收任何物理资源。所以需要finally进行物理资源的显示回收。
Java7自动关闭资源的try语句
由于finally语句较臃肿,Java7允许try关键字后面跟一对圆括号,括号中声明或初始化那些必须在程序结束时显示关闭的资源。
这些资源必须实现AutoCloseable接口或其子接口Closeable接口。AutoCloseable接口里的close()方法声明抛出了Exception,可以抛出任何异常;而其子接口Closeable接口里的close()方法声明抛出了IOException,只能抛出IOException及其子类。
1 |
|
上面粗体字分别声明、初始化了两个IO流,由于BufferedReader和PrintStream都实现了Closeable接口,而且他们放在try块中声明、初始化,所以try语句会自动关闭他们。
Java7几乎把所有的“资源类”(包括IO的各种类、JDBC编程的Connection、Statement等接口)进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口。
Checked异常和Runtime异常
Java的Exception又可分为Checked异常和Runtime异常(运行时异常)。
Checked异常体现了Java的严谨性:要么显式声明抛出,要么显式捕获并处理它,不能对Checked异常不闻不问。但多数方法有不能明确地直到如何处理异常,所以Checked也有劣势。
*使用throws声明抛出异常 *
当前不知如何处理这种类型的异常->交给上一级调用者处理->如果main()也不知如何处理->交给JVM,JVM打印跟踪栈信息,并终止程序。
使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,该异常交给JVM处理。JVM处理异常的方法是,打印异常的跟踪栈信息,并中止程序运行(这就是程序遇到异常自动结束的原因)。
throws只能在方法签名中使用,可以抛出多个异常类,多个异常类用逗号隔开。
一旦使用throws抛出异常,程序就无须使用try…catch块来捕获异常了。
1 | public class ThrowsTest{ |
上面程序不处理IOException异常,该异常交给JVM处理,所以程序会打印该异常的跟踪栈信息,并结束程序。
如果某段代码中调用了一个throws声明的方法,该方法声明抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。即调用该方法时要么放在try…catch块中,要么放在另一个带throws声明抛出的方法中。
1 | public class ThrowTest2{ |
大部分时候推荐使用Runtime异常,而不使用Checked异常。
程序的健壮性(robustness):即程序在执行过程中处理错误、异常时继续正常运行的能力。
throw自行抛出异常
当程序出现错误时,系统会自动抛出异常;如果需要在程序中自行抛出异常,则应使用throw语句,它抛出的是一个异常实例,而不是异常类。
当Java运行时接收到开发者自行抛出的异常时,同样会终止当前的执行流,跳转到该异常对应的catch块,由该catch块处理该异常。
自定义异常类
(1) 自定义异常应该继承Exception基类。Exception是检查型异常,编译时就出现异常,在程序中必须使用try…catch进行处理;
(2) 如果自定义Runtime异常,则应该继承RuntimeException基类。RuntimeException是非检查型异常,编译时不检查,运行时出现异常,例如NumberFormatException,可以不使用try…catch进行处理(但最好也用try…catch捕获),但是如果产生异常,则异常将由JVM进行处理。
编译出现异常差不多就是在eclipse中写代码时,代码下面出现红色波浪线。
自定义异常类通常需要定义两个构造器:一个无参构造器,一个带字符串参数的构造器。这两个构造器作为异常对象getMessage()方法的返回值。
1 | public class AuctionException extends Exception{ |
throw一般与catch、throws同时使用
前面介绍的异常处理方式有如下两种:
1) 在出现异常的方法内捕获并处理该异常,该方法的调用者不能再次捕获该异常。(即前面的try..catch结构)
2) 该方法签名中声明抛出该异常,将该异常交给方法调用者处理。(即前面的throws结构)
然而实际情况是,在异常出现的当前方法中,可能程序只对异常进行部分处理,还有些处理需要交给该方法的调用者中完成,所以应该再次抛出异常,让该方法的调用者也能捕获到该异常。
1 | /* |
1 | /*利用catch块结合throw语句,可以实现 |
在catch块捕获到异常后,系统打印了该异常的跟踪栈信息,接着又抛出了一个AuctionException异常,通知该方法的调用者再次处理该异常,即main方法中的bid()方法还可以再次捕获AuctionException异常,并将该异常的详细描述输出到标准错误输出。
异常链
把原始异常信息隐藏起来,仅向上提供必要的异常提示信息,可以保证底层异常不会扩散到表现层,避免向上暴露太多的实现细节。
异常链:捕获一个异常后接着抛出另一个异常,并把原始信息保存下来。
1 | public calSal() throws SalException{ |
上面创建SalException对象时,传入了一个Exception对象,而不是String对象,这就需要SalException类有相应的构造器。Throwable基类有一个可以接收Exception参数的方法,可以如下定义SalException类:
1 | public class SalException extends Exception{ |
异常跟踪栈
异常对象的printStackTrace()方法用于打印异常的跟踪栈信息。
1 | //自定义SelfException类 |
输出:
1 | com.Licht._08.PrintStackTraceTest.SelfException: 自定义异常信息 |
异常处理规划
不要过度使用异常
只有对外部的、不能确定和预知的运行时错误才使用异常。
异常处理机制的初衷是将不可预期异常的处理代码和正常业务逻辑处理代码分离,因此绝对不要使用异常处理来代替正常的业务逻辑判断。而且异常机制的效率比正常的流程控制效率差。
不要使用过于庞大的try块
正确的做法是,把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。
避免使用Catch All语句
不要忽略异常
既然已经捕获到异常,那catch块就应该处理并修复这个错误。